<?php
use frontend\modules\yii2\widgets\layouts\markdown\Markdown;

$this->title = 'Yii的模型（Model）使用教程-数据校验器rules规则大全';
$this->registerMetaTag([
    'name' => 'keywords',
    'content' => 'yii,yii2,框架,应用结构,model，模型,模型类,使用,教程,rules,校验器,场景'
]);
$this->registerMetaTag([
    'name' => 'description',
    'content' => '对Yii2框架的模型（Model）进行详细的介绍'
]);
$this->registerMetaTag([
    'name' => 'author',
    'content' => 'chenzhiwei'
]);


$html = [
    "模型类" => <<<HTML
# Yii的模型（Model）使用教程
模型是`MVC`设计模式的重要组成部分，它负责管理数据，通常会按照数据库中数据表的结构创建与之对应的模型类，在模型类中实现增删改查等方法实现对数据的管理。正是因为面向对象的继承特性，才得以让我们可以封装不同种类数据库（Mysql/Oracle/Mssql...）的底层实现，只向外提供同样规则的操作方法，即能提高安全性，也能提高工作效率。

## 模型类
通过数据来源的不同，一般我们会用到两种模型类：
- [yii\base\Model](https://www.yiichina.com/doc/api/2.0/yii-base-model)  
数据来自于非数据库，很多情况下我们的业务涉及到的表单信息不需要存储到数据库（如：ContactForm 以邮件方式发送信息给我们），因此没有对应的数据表，但需要用到模型类提供的数据校验机制，此时需要根据业务涉及到的表单字段创建一个继承于 [yii\base\Model](https://www.yiichina.com/doc/api/2.0/yii-base-model) 的模型类；
- [yii\db\ActiveRecord](https://www.yiichina.com/doc/api/2.0/yii-db-activerecord)  
数据来自于数据库，此类是 [yii\base\Model](https://www.yiichina.com/doc/api/2.0/yii-base-model) 类的子类，但是扩展了很多针对数据库操作的方法，要求必须要填写数据表名。

可通过继承 [yii\base\Model](https://www.yiichina.com/doc/api/2.0/yii-base-model) 或它的子类定义模型类， 基类 [yii\base\Model](https://www.yiichina.com/doc/api/2.0/yii-base-model) 支持许多实用的特性：
- `属性`: 代表可像普通类属性或数组 一样被访问的业务数据;
- `属性标签`: 指定属性显示出来的标签;
- `块赋值`: 支持一步给许多属性赋值;
- `验证规则`: 确保输入数据符合所申明的验证规则;
- `数据导出`: 允许模型数据导出为自定义格式的数组。

先睹为快，下面的是`Contact`的模型类：
```php
<?php

namespace frontend\models;

use Yii;
use yii\base\Model;

/**
 * ContactForm is the model behind the contact form.
 */
class ContactForm extends Model
{
    public \$name;
    public \$email;
    public \$subject;
    public \$body;
    public \$verifyCode;


    /**
     * {@inheritdoc}
     */
    public function rules()
    {
        return [
            // name, email, subject and body are required
            [['name', 'email', 'subject', 'body'], 'required'],
            // email has to be a valid email address
            ['email', 'email'],
            // verifyCode needs to be entered correctly
            ['verifyCode', 'captcha'],
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function attributeLabels()
    {
        return [
            'name' => 'Your name',
            'email' => 'Your email address',
            'subject' => 'Subject',
            'body' => 'Content',
            'verifyCode' => 'Verification Code',
        ];
    }
}
```
HTML,
    "属性" => <<<HTML
## 属性
模型类中的属性对应着存储数据的字段名.

### 定义属性
下面的代码就是对`属性`的定义：
```php
namespace app\models;
use yii\base\Model;

class ContactForm extends Model
{
    public \$name;
    public \$email;
    public \$subject;
    public \$body;
    public \$verifyCode;
    
    ... ...
}
```
> 或许你会问：`ContactForm`并没有与数据库中任何一张表对应啊，为什么要创建这个模型类呢？

虽然上述定义的`属性`并没有和数组库中的任何字段有对应关系，但是它们却和html中的表单里的字段有对应关系，模型类的数据一方面来自于数据库，而另一方面也来自于用户的输入。来自于用户的数据有很多情况是不需要存储到数据库的，因此没有数据表与其对应也很正常，但是用户提交的数据是一定需要Yii处理的，而模型类能够完美兼容这种情况。

### 属性标签
如果你有过前端开发的经验，那就会知道很多时候展示给用户的字段名称和数据表中的字段名称是不一样的，`属性标签`可以让你在模型类中定义需要展现给用户看的字段名称：
```php
public function attributeLabels()
{
    return [
        'name' => 'Your name',
        'email' => 'Your email address',
        'subject' => 'Subject',
        'body' => 'Content',
        'verifyCode' => 'Verification Code',
    ];
}
```
> 注意：属性标签明显是给在视图里用的，`ActiveField`小部件承担起了模型和视图的数据交互功能。

### 块赋值
块赋值只用一行代码将用户所有输入填充到一个模型，非常方便， 它直接将输入数据对应填充到 [yii\base\Model::attributes()](https://www.yiichina.com/doc/api/2.0/yii-base-model#attributes()-detail) 属性。
```php
\$device = new Device();
\$device->attributes = [
    "id" => "2",
    "name" => "Switch Model 1",
    "isOnOff" => "1"
];
```
上述块赋值代码和下面的代码是一样的效果：
```php
\$device = new Device();
\$data = [
    "id" => "2",
    "name" => "Switch Model 1",
    "isOnOff" => "1"
];
\$device->id = isset(\$data["id"]) ? \$data["id"] : null;
\$device->name = isset(\$data["name"]) ? \$data["name"] : null;
\$device->isOnOff = isset(\$data["isOnOff"]) ? \$data["isOnOff"] : null;
```
对比这两种赋值方式，很明显块赋值简洁明了不易出错，具有先天的优越性。
> 注意：块赋值成功的前提是所操作的属性已经在场景（[scenarios](#scenario)）中定义过，即：在模型类`rules()`或`scenarios()`方法中设置过的属性，如果没有指定场景，则默认使用`default`场景。

HTML,
    "场景" => <<<HTML
## 场景（scenario） id=scenario
这个东西很有用，它可以对属性进行分组，你可以对任意一组的属性进行单独处理，使用场景主要有两方面：
- 属性块赋值
- 数据验证

很抽象？我们来举个例子：
```php
class Device extends yii\base\Model
{
    public \$id;         // 设备id
    public \$name;       // 设备名称
    public \$brightness; // 灯光亮度：只属于灯的属性
    public \$isOnOff;    // 开或者关状态：所有设备都具备的属性
    public \$color;      // 灯光颜色：只属于灯的属性
    
    // 数据校验规则
    public function rules()
    {
        return [
            // id, name, isOnOff 是light设备和switch设备都必须要有的属性
            [["id", "name", "isOnOff", "brightness", "color"], "required"],
            // brightness亮度只能是light的属性，取值范围是从 0 到 100
            ["brightness", "in", "range" => range(0, 100)],
            // color灯色也只能是light的属性，取值范围只能是红、黄、蓝
            ["color", "in", "range" => ["red", "yellow", "blue"]],
        ];
    }
    
    // 用场景对属性进行分组
    public function scenarios()
    {
        \$scenarios = parent::scenarios();

        // 定义灯设备场景下的属性列表
        \$scenarios["light"] = ["id", "name", "isOnOff", "color", "brightness"];
        // 定义开关面板设备的属性列表
        \$scenarios["switch"] = ["id", "name","isOnOff"];
        
        return \$scenarios;
    }
}
```
上述代码定义了一个设备模型类，但是不同的设备的属性不一样，如：开关设备（switch）并不需要灯光亮度和灯光颜色，因此可以通过`场景`来指定属性分组，使用方法如下：
```php
\$device = new Device();
// 指定场景，即：指定参与本此数据块赋值和数据校验的属性有哪些
\$device->scenario = "switch";
// 使用场景进行块赋值，此处即使你传入了`color`的值也不会生效，因为`switch`场景下没有`color`属性。
\$device->attributes = [
    "id" => 1,
    "name" => "Switch Model 3",
    "isOnOff" => 1
];
// 使用场景进行数据校验，只对`swtich`场景下的属性进行校验
if (!\$device->validate()) {
    var_dump(\$device->getFirstErrors());
}
```
> 虽然在`rules()`中指定了`brightness`和`color`属性是必填属性，但是由于我们没有把这两个属性加入到场景`switch`下，因此校验器不会对它们进行校验。

### 定义场景
定义模型类的场景有两种方式，第一种就是上述参考代码所描述的覆写`scenarios()`方法，这种方式的优势是验证规则和属性分组分开管理，各司其职，清晰明了:
```php
public function rules()
{
    return [
        // id, name, isOnOff 是light设备和switch设备都必须要有的属性
        [["id", "name", "isOnOff", "brightness", "color"], "required"],
        // brightness亮度只能是light的属性，取值范围是从 0 到 100
        ["brightness", "in", "range" => range(0, 100)],
        // color灯色也只能是light的属性，取值范围只能是红、黄、蓝
        ["color", "in", "range" => ["red", "yellow", "blue"]],
    ];
}

// 用场景对属性进行分组
public function scenarios()
{
    \$scenarios = parent::scenarios();

    // 定义灯设备场景下的属性列表
    \$scenarios["light"] = ["id", "name", "isOnOff", "color", "brightness"];
    // 定义开关面板设备的属性列表
    \$scenarios["switch"] = ["id", "name","isOnOff"];
    
    return \$scenarios;
}

```

另外一种方法是在规则中指定场景，完全使用这种方式时，如果设备种类过多，设备属性过多，相信你会发疯的：
```php
public function rules()
{
    return [
        // id, name, isOnOff 是light设备和switch设备都必须要有的属性
        [["id", "name", "isOnOff"], "required", "on" => ["light", "switch"]],
        // brightness亮度只能是light的属性，取值范围是从 0 到 100
        ["brightness", "in", "range" => range(0, 100), "on" => ["light"]],
        // color灯色也只能是light的属性，取值范围只能是红、黄、蓝
        ["color", "in", "range" => ["red", "yellow", "blue"], "on" => ["light"]],
    ];
}
```
但是这种方式确是必不可少的，考虑这种情况： 

`isOnOff`是`switch`设备和`light`设备的公共属性，但是业务规定提交`light`设备的表单时必须要填`isOnOff`，而`switch`设备则可以不填，这种情况你是无法通过第一种场景定义法实现的，需要用到第二种在`rules()`中指定场景：
```php
public function rules()
{
    return [
        // light设备必须要赋予isOnOff属性的值
        ["isOnOff", "required", "on" => ["light"]],
        
        // isOnOff同时属于Light设备和Switch设备
        ["isOnOff", "in", "range" => ["0", "1"], "on" => ["light", "switch"]],
    ];
}
```
上述第二条规则将`isOnOff`设置为`light`和`switch`的共同属性，但第一条规则又强制了在`light`场景下`isOnOff`是必填属性，满足了我们的业务场景。


HTML,
    "toArray" => <<<HTML
## 模型转数组

在`PHP`中，处理数据的主要工具是数组，因此把模型类的实例化对象中的数据转化为数组格式就尤为重要，举个例子：
```php
// 通过模型类查询数据
\$user = User::findOne(["id" => 1]);
```
上述的`\$user`变量存储的其实是一个对象，如果对象中存储的是一条记录还好说，我们可以通过`\$user->username`类访问数据，但是如果查询出的是多条记录呢：
```php
\$users = User::find()->all();
```
此时`\$users`是一个数组，但每个元素都是一个对象，每个对象中保存一行数据，你必须通过`\$user[0]->username`的方式访问数据，这对我们处理数据带来了很大困难，因此`Yii`提供了`toArray()`方法，把模型类中的数据转化为数组。

### toArray()
转化数组的方式很简单，但是对单行数据和多行数据的使用有些差别：
```php
// 当行数据 对象转数组
\$user = User::findOne(["id" => 1])->toArray();

// 多行数据 对象转数组
\$users = array_map(function(\$user){
    return \$user->toArray();
}, User::find()->all());

// 多行数据 对象转数组，简便写法
\$users = User::find()->asArray()->all();
```
> 注意：`toArray()`可以结合模型类的`fields()`方法一起使用，在`fields()`中完成数据预处理，而`asArray()`并没有这个功能。

### fields()
这个方法是配合`toArray()`导出数据前进行数据处理的地方，使用举例如下：
```php
class User extends Model
{

    public \$firstName;
    public \$lastName;
    public \$birthday;

    public function rules()
    {
        return [
            [["firstName", "lastName", "birthday"], "required"]
        ];
    }

    public function fields()
    {
        return [
            "name" => function(){
                return \$this->firstName . " · " . \$this->lastName;
            },
            "birthday" => function(){
                return date("Y-m-d", \$this->birthday);
            }
        ];
    }
}

\$user = new User();
\$user->attributes = [
    "firstName" => "杰森",
    "lastName" => "斯坦森",
    "birthday" => strtotime("1967/7/26")
];
print_r(\$user->toArray());
// 输出：
// Array ( [name] => 杰森 · 斯坦森 [birthday] => 1967-07-26 )
```

过滤某些字段：
```php
// 过滤掉一些字段，特别用于
// 你想继承父类实现并不想用一些敏感字段
public function fields()
{
    \$fields = parent::fields();

    // 去掉一些包含敏感信息的字段
    unset(\$fields['auth_key'], \$fields['password_hash'], \$fields['password_reset_token']);

    return \$fields;
}
```

### extraFields()
这个方法用于新增`toArray()`导出的字段，使用方法如下：
```php
class User extends Model
{

    public \$firstName;
    public \$lastName;
    public \$birthday;

    public function rules()
    {
        return [
            [["firstName", "lastName", "birthday"], "required"]
        ];
    }

    public function fields()
    {
        return [
            "name" => function(){
                return \$this->firstName . " · " . \$this->lastName;
            },
            "birthday" => function(){
                return date("Y-m-d", \$this->birthday);
            }
        ];
    }

    // 额外添加的字段
    public function extraFields()
    {
        return [
            "country" => function(){
                if (\$this->lastName == "斯坦森") {
                    return "美国";
                } else {
                    return "中国";
                }
            }
        ];
    }
}

\$user = new User();
\$user->attributes = [
    "firstName" => "杰森",
    "lastName" => "斯坦森",
    "birthday" => strtotime("1967/7/26")
];
print_r(\$user->toArray([], ["country"])));
// 输出：
// Array ( [name] => 杰森 · 斯坦森 [birthday] => 1967-07-26 )
```
上述代码中`toArray([], ["country"])`的第一个额参数`[]`表示使用`fields()`中的所有字段，而`["country"]`表示使用`extraFields()`中定义的`country`字段。


HTML,

];




echo Markdown::widget(["content" => implode("\n", $html)]);

